// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.linqingying.gitcode.pullrequest.ui

import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.util.Disposer
import com.intellij.util.concurrency.annotations.RequiresEdt
import java.util.concurrent.CancellationException
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionException
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executor
import java.util.concurrent.atomic.AtomicReference
import java.util.function.BiFunction
import kotlin.properties.Delegates.observable

class GCCompletableFutureLoadingModel<T>(parentDisposable: Disposable) : GCSimpleLoadingModel<T>(), Disposable {

    //to cancel old callbacks
    private var updateFuture by observable<CompletableFuture<Unit>?>(null) { _, oldValue, _ ->
        oldValue?.cancel(true)
    }

    init {
        Disposer.register(parentDisposable, this)
    }

    @set:RequiresEdt
    @get:RequiresEdt
    var future by observable<CompletableFuture<T>?>(null) { _, _, newValue ->
        if (Disposer.isDisposed(this)) return@observable
        if (newValue != null) load(newValue) else reset()
    }

    private fun load(future: CompletableFuture<T>) {
        error = null
        loading = true
        eventDispatcher.multicaster.onLoadingStarted()
        updateFuture = future.handleOnEdt(this) { result, error ->
            if (error != null && !isCancellation(error)) {
                this.error = error
                resultAvailable = false
            } else {
                this.result = result
                resultAvailable = true
            }
            loading = false
            eventDispatcher.multicaster.onLoadingCompleted()
        }
    }

    private fun reset() {
        updateFuture = null
        loading = false
        result = null
        resultAvailable = false
        error = null
        eventDispatcher.multicaster.onReset()
    }

    override fun dispose() {
        future = null
    }
}

fun extractError(error: Throwable): Throwable {
    return when (error) {
        is CompletionException -> extractError(error.cause!!)
        is ExecutionException -> extractError(error.cause!!)
        else -> error
    }
}


fun <T> CompletableFuture<T>.handleOnEdt(
    disposable: Disposable,
    handler: (T?, Throwable?) -> Unit
): CompletableFuture<Unit> {
    val handlerReference = AtomicReference(handler)
    Disposer.register(disposable) {
        handlerReference.set(null)
    }

    return handleAsync(BiFunction<T?, Throwable?, Unit> { result: T?, error: Throwable? ->
        val handlerFromRef = handlerReference.get() ?: throw ProcessCanceledException()
        handlerFromRef(result, error?.let { extractError(it) })
    }, getEDTExecutor(null))
}

fun getEDTExecutor(modalityState: ModalityState? = null) =
    Executor { runnable -> runInEdt(modalityState) { runnable.run() } }
fun isCancellation(error: Throwable): Boolean {
    return error is ProcessCanceledException
            || error is CancellationException
            || error is InterruptedException
            || error.cause?.let(::isCancellation) ?: false
}
